import fg from "fast-glob";
import fs from "fs-extra";
import path from "path";
import _ from "lodash";
import { Project, SyntaxKind, Node } from "ts-morph";
import { Chaos } from "./Chaos.js";
import { toolchain } from "../toolchain.js";

interface IReserveCfg {
    structs: {
        name: string
        locale: string
    }[]
}

export class JsonTerser {
    private readonly ignoreJsons = ['bundle.json', 'dataVer.json'];
    private keyCntMap: Record<string, number> = {};

    // 由于存在union类型的使用情形，需保证所有相同的key混淆结果一致
    private cache: Record<string, string> = {};

    async run(workSpacePath: string): Promise<void> {
        const cacheFile = path.join(workSpacePath, '.build', 'obfusecation.json.cache');
        if (fs.existsSync(cacheFile)) {
            this.cache = await fs.readJSON(cacheFile);
        }
        // 读取保护字段
        const reserveds = await this.readReserveds(workSpacePath);
        console.log('The following fields will not be tersered: ' + reserveds.join(', '));

        // 统计频次并预分配混淆结果
        await this.statKeys(workSpacePath);
        // 保存cache
        await fs.ensureDir(path.dirname(cacheFile));
        await fs.writeJson(cacheFile, this.cache, { spaces: 2 });
        if (toolchain.option.executeTpl == 'OnlyCode') {
            // 混淆ts
            await this.obfusecateTs(workSpacePath, reserveds);
        } else if (toolchain.option.executeTpl == 'OnlyJson') {
            // 混淆json
            await this.obfusecateJson(workSpacePath, reserveds);
        } else {
            // 混淆json
            await this.obfusecateJson(workSpacePath, reserveds);
            // 混淆ts
            await this.obfusecateTs(workSpacePath, reserveds);
        }
    }

    /**读取保护字段配置 */
    private async readReserveds(workSpacePath: string): Promise<string[]> {
        const reserveds: string[] = [];

        const cfgFile = path.join(workSpacePath, 'build-tools/cfg/terserReserve.json');
        if (fs.existsSync(cfgFile)) {
            const tsRoot = path.join(workSpacePath, 'TsScripts');
            const project = new Project({
                tsConfigFilePath: path.join(tsRoot, 'tsconfig.json')
            });

            const cfg = await fs.readJSON(cfgFile) as IReserveCfg;
            for (const st of cfg.structs) {
                let stFile = st.locale, stName = st.name;
                if (stName.startsWith('GameConfig.')) {
                    stFile = 'System/data/GameConfig.d.ts';
                    stName = stName.substring(11);
                }

                const src = project.getSourceFile(path.join(tsRoot, stFile))!;
                const hit = this.findChildInSource(src, (v) => v.isKind(SyntaxKind.InterfaceDeclaration) && v.getName() == stName);
                if (hit == null) {
                    throw '[JsonTerser] Cannot find interface: ' + st.name;
                }
                if (hit.isKind(SyntaxKind.InterfaceDeclaration)) {
                    const pps = hit.getProperties();
                    for (const p of pps) {
                        reserveds.push(p.getName());
                    }
                }
            }
        }
        return _.uniq(reserveds);
    }

    private findChildInSource(n: Node, condition: (n: Node) => boolean): Node | null {
        const cs = n.getChildren();
        for (const c of cs) {
            if (condition(c)) {
                return c;
            }
            const cc = this.findChildInSource(c, condition);
            if (cc != null) {
                return cc;
            }
        }
        return null;
    }

    private async obfusecateTs(workSpacePath: string, reserveds: string[]): Promise<void> {
        console.log('start obfusecating typescripts, please wait about 5 mins...');
        
        const tsRoot = path.join(workSpacePath, 'TsScripts');
        const gameConfigTs = path.join(tsRoot, 'System/data/GameConfig.d.ts');
        const gameConfigContent = await await fs.readFile(gameConfigTs, 'utf-8');
        if (gameConfigContent.includes('_: ')) {
            console.log('typescript already obfusecated!');
            return;
        }
        
        const project = new Project({
            tsConfigFilePath: path.join(tsRoot, 'tsconfig.json')
        });

        const gameConfigSrc = project.getSourceFile(gameConfigTs)!;
        const gcm = gameConfigSrc.getModuleOrThrow('GameConfig');
        const itfs = gcm.getInterfaces();
        for (const itf of itfs) {
            const ppts = itf.getProperties();
            for (let i = 0, len = ppts.length; i < len; i++) {
                const p = ppts[i];
                const name = p.getName();
                if (reserveds.includes(name)) continue;
                // let t = p.getType();
                // const aet = t.getArrayElementType();
                // if (aet != null) {
                //     t = aet;
                // }
                const newName = this.cache[name];
                if (newName) {
                    p.rename(newName);
                }
            }
        }
        await project.save();
    }

    private async statKeys(workSpacePath: string): Promise<void> {
        // 统计所有key的使用情形
        const jsonRoot = path.join(workSpacePath, 'Assets/AssetSources/data');
        const jsons = await fg('*.json', { cwd: jsonRoot, ignore: this.ignoreJsons });
        for (const j of jsons) {
            const jfile = path.join(jsonRoot, j);
            const json = await fs.readJSON(jfile);
            for (const obj of json) {
                this.statObjKeys(obj);
            }
        }
        // 按使用频次排序
        const allKeys = Object.keys(this.keyCntMap);
        allKeys.sort((a, b) => this.keyCntMap[b] - this.keyCntMap[a]);
        const minCache: Record<string, string> = {};
        for (const key of allKeys) {
            const cv = this.cache[key];
            if (cv) {
                minCache[key] = this.cache[key];
            }
        }
        const chaos = new Chaos();
        chaos.init(allKeys.length, minCache);

        for (const key of allKeys) {
            if (key.length < 3) continue;
            if (!minCache[key]) {
                minCache[key] = chaos.getChaos(key);
            }
        }
        this.cache = minCache;
    }

    private statObjKeys(obj: any): void {
        // 统计所有key的使用频次
        for (let key in obj) {
            this.keyCntMap[key] = (this.keyCntMap[key] || 0) + 1;
            const value = obj[key];
            if (value instanceof Array) {
                for (const sv of value) {
                    if (typeof(sv) === 'object') this.statObjKeys(sv);
                }
            } else if (typeof(value) === 'object') {
                this.statObjKeys(value);
            }
        }
    }

    private async obfusecateJson(workSpacePath: string, reserveds: string[]): Promise<void> {
        const jsonRoot = path.join(workSpacePath, 'Assets/AssetSources/data');
        const jsons = await fg('*.json', { cwd: jsonRoot, ignore: this.ignoreJsons });

        const bundle: Record<string, any[]> = {};
        for (const j of jsons) {
            const jfile = path.join(jsonRoot, j);
            const jsonContent = await fs.readFile(jfile, 'utf-8');
            const newContent = jsonContent.replaceAll(/(?<=")\w+(?=":)/g, (s) => !reserveds.includes(s) && this.cache[s] || s);
            await fs.writeFile(jfile, newContent, 'utf-8');

            bundle[path.basename(j, '.json')] = JSON.parse(newContent);
        }

        await fs.writeJSON(path.join(jsonRoot, 'bundle.json'), bundle);
    }
}
